package cn.jingyuan.bee.utils;

import cn.jingyuan.bee.utils.exceptions.UtilsException;
import cn.jingyuan.bee.utils.io.FastByteArrayOutputStream;
import cn.jingyuan.bee.utils.io.FileUtils;
import cn.jingyuan.bee.utils.io.IORuntimeException;
import cn.jingyuan.bee.utils.io.IoUtils;

import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.*;

/**
 * 压缩工具类
 */
public class ZipUtils {

    private static final int DEFAULT_BYTE_ARRAY_LENGTH = 32;

    /** 默认编码，使用平台相关编码 */
    private static final Charset DEFAULT_CHARSET = CharsetUtils.defaultCharset();

    /**
     * 打包到当前目录，使用默认编码 UTF-8
     *
     * @param srcPath 源文件路径
     *
     * @return 打包好的压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(String srcPath) throws UtilsException {
        return zip(srcPath, DEFAULT_CHARSET);
    }

    /**
     * 打包到当前目录
     *
     * @param srcPath 源文件路径
     * @param charset 编码
     *
     * @return 打包好的压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(String srcPath, Charset charset) throws UtilsException {
        return zip(FileUtils.file(srcPath), charset);
    }

    /**
     * 打包到当前目录，使用默认编码 UTF-8
     *
     * @param srcFile 源文件或目录
     *
     * @return 打包好的压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File srcFile) throws UtilsException {
        return zip(srcFile, DEFAULT_CHARSET);
    }

    /**
     * 打包到当前目录
     *
     * @param srcFile 源文件或目录
     * @param charset 编码
     *
     * @return 打包好的压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File srcFile, Charset charset) throws UtilsException {
        final File zipFile = FileUtils.file(srcFile.getParentFile(), FileUtils.mainName(srcFile) + ".zip");
        zip(zipFile, charset, false, srcFile);
        return zipFile;
    }

    /**
     * 对文件或文件目录进行压缩<br>
     * 不包含被打包目录
     *
     * @param srcPath 要压缩的源文件路径。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
     * @param zipPath 压缩文件保存的路径，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     *
     * @return 压缩好的 Zip 文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(String srcPath, String zipPath) throws UtilsException {
        return zip(srcPath, zipPath, false);
    }

    /**
     * 对文件或文件目录进行压缩
     *
     * @param srcPath 要压缩的源文件路径。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
     * @param zipPath 压缩文件保存的路径，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param withSrcDir 是否包含被打包目录
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(String srcPath, String zipPath, boolean withSrcDir) throws UtilsException {
        return zip(srcPath, zipPath, DEFAULT_CHARSET, withSrcDir);
    }

    /**
     * 对文件或文件目录进行压缩
     *
     * @param srcPath 要压缩的源文件路径。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
     * @param zipPath 压缩文件保存的路径，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param charset 编码
     * @param withSrcDir 是否包含被打包目录
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(String srcPath, String zipPath, Charset charset, boolean withSrcDir) throws UtilsException {
        final File srcFile = FileUtils.file(srcPath);
        final File zipFile = FileUtils.file(zipPath);
        zip(zipFile, charset, withSrcDir, srcFile);
        return zipFile;
    }

    /**
     * 对文件或文件目录进行压缩<br>
     * 使用默认 UTF-8 编码
     *
     * @param zipFile 生成的 Zip 文件，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为 false，则只压缩目录下的文件或目录，为 true 则将本目录也压缩
     * @param srcFiles 要压缩的源文件或目录。
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File zipFile, boolean withSrcDir, File... srcFiles) throws UtilsException {
        return zip(zipFile, DEFAULT_CHARSET, withSrcDir, srcFiles);
    }

    /**
     * 对文件或文件目录进行压缩
     *
     * @param zipFile 生成的 Zip 文件，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param charset 编码
     * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为 false，则只压缩目录下的文件或目录，为 true 则将本目录也压缩
     * @param srcFiles 要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File zipFile, Charset charset, boolean withSrcDir, File... srcFiles) throws UtilsException {
        return zip(zipFile, charset, withSrcDir, null, srcFiles);
    }

    /**
     * 对文件或文件目录进行压缩
     *
     * @param zipFile 生成的 Zip 文件，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param charset 编码
     * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为 false，则只压缩目录下的文件或目录，为 true 则将本目录也压缩
     * @param filter 文件过滤器，通过实现此接口，自定义要过滤的文件（过滤掉哪些文件或文件夹不加入压缩）
     * @param srcFiles 要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File zipFile, Charset charset, boolean withSrcDir, FileFilter filter, File... srcFiles) throws UtilsException {
        validateFiles(zipFile, srcFiles);

        try (ZipOutputStream out = getZipOutputStream(zipFile, charset)) {
            String srcRootDir;
            for (File srcFile : srcFiles) {
                if (null == srcFile) {
                    continue;
                }
                // 如果只是压缩一个文件，则需要截取该文件的父目录
                srcRootDir = srcFile.getCanonicalPath();
                if (srcFile.isFile() || withSrcDir) {
                    // 若是文件，则将父目录完整路径都截取掉；若设置包含目录，则将上级目录全部截取掉，保留本目录名
                    srcRootDir = srcFile.getCanonicalFile().getParentFile().getCanonicalPath();
                }
                // 调用递归压缩方法进行目录或文件压缩
                zip(srcFile, srcRootDir, out, filter);
                out.flush();
            }
        } catch (IOException e) {
            throw new UtilsException(e);
        }
        return zipFile;
    }

    /**
     * 对流中的数据加入到压缩文件，使用默认 UTF-8 编码
     *
     * @param zipFile 生成的 Zip 文件，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param path 流数据在压缩文件中的路径或文件名
     * @param data 要压缩的数据
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File zipFile, String path, String data) throws UtilsException {
        return zip(zipFile, path, data, DEFAULT_CHARSET);
    }

    /**
     * 对流中的数据加入到压缩文件
     *
     * @param zipFile 生成的 Zip 文件，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param path 流数据在压缩文件中的路径或文件名
     * @param data 要压缩的数据
     * @param charset 编码
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File zipFile, String path, String data, Charset charset) throws UtilsException {
        return zip(zipFile, path, IoUtils.toStream(data, charset), charset);
    }

    /**
     * 对流中的数据加入到压缩文件<br>
     * 使用默认编码 UTF-8
     *
     * @param zipFile 生成的 Zip 文件，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param path 流数据在压缩文件中的路径或文件名
     * @param in 要压缩的源
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File zipFile, String path, InputStream in) throws UtilsException {
        return zip(zipFile, path, in, DEFAULT_CHARSET);
    }

    /**
     * 对流中的数据加入到压缩文件
     *
     * @param zipFile 生成的 Zip 文件，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param path 流数据在压缩文件中的路径或文件名
     * @param in 要压缩的源，默认关闭
     * @param charset 编码
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File zipFile, String path, InputStream in, Charset charset) throws UtilsException {
        return zip(zipFile, new String[]{path}, new InputStream[]{in}, charset);
    }

    /**
     * 对流中的数据加入到压缩文件<br>
     * 路径列表和流列表长度必须一致
     *
     * @param zipFile 生成的 Zip 文件，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param paths 流数据在压缩文件中的路径或文件名
     * @param ins 要压缩的源
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File zipFile, String[] paths, InputStream[] ins) throws UtilsException {
        return zip(zipFile, paths, ins, DEFAULT_CHARSET);
    }

    /**
     * 对流中的数据加入到压缩文件<br>
     * 路径列表和流列表长度必须一致
     *
     * @param zipFile 生成的 Zip 文件，包括文件名。注意：zipPath 不能是 srcPath 路径下的子文件夹
     * @param paths 流数据在压缩文件中的路径或文件名
     * @param ins 要压缩的源，添加完成后自动关闭流
     * @param charset 编码
     *
     * @return 压缩文件
     *
     * @throws UtilsException IO 异常
     */
    public static File zip(File zipFile, String[] paths, InputStream[] ins, Charset charset) throws UtilsException {
        if (ArrayUtils.isEmpty(paths) || ArrayUtils.isEmpty(ins)) {
            throw new IllegalArgumentException("Paths or ins is empty !");
        }
        if (paths.length != ins.length) {
            throw new IllegalArgumentException("Paths length is not equals to ins length !");
        }

        ZipOutputStream out = null;
        try {
            out = getZipOutputStream(zipFile, charset);
            for (int i = 0; i < paths.length; i++) {
                addFile(ins[i], paths[i], out);
            }
        } finally {
            IoUtils.close(out);
        }
        return zipFile;
    }

    /**
     * 解压到文件名相同的目录中，默认编码 UTF-8
     *
     * @param zipFilePath 压缩文件路径
     *
     * @return 解压的目录
     *
     * @throws UtilsException IO 异常
     */
    public static File unzip(String zipFilePath) throws UtilsException {
        return unzip(zipFilePath, DEFAULT_CHARSET);
    }

    /**
     * 解压到文件名相同的目录中
     *
     * @param zipFilePath 压缩文件路径
     * @param charset 编码
     *
     * @return 解压的目录
     *
     * @throws UtilsException IO 异常
     */
    public static File unzip(String zipFilePath, Charset charset) throws UtilsException {
        return unzip(FileUtils.file(zipFilePath), charset);
    }

    /**
     * 解压到文件名相同的目录中，使用 UTF-8 编码
     *
     * @param zipFile 压缩文件
     *
     * @return 解压的目录
     *
     * @throws UtilsException IO 异常
     */
    public static File unzip(File zipFile) throws UtilsException {
        return unzip(zipFile, DEFAULT_CHARSET);
    }

    /**
     * 解压到文件名相同的目录中
     *
     * @param zipFile 压缩文件
     * @param charset 编码
     *
     * @return 解压的目录
     *
     * @throws UtilsException IO 异常
     */
    public static File unzip(File zipFile, Charset charset) throws UtilsException {
        return unzip(zipFile, FileUtils.file(zipFile.getParentFile(), FileUtils.mainName(zipFile)), charset);
    }

    /**
     * 解压，默认 UTF-8 编码
     *
     * @param zipFilePath 压缩文件的路径
     * @param outFileDir 解压到的目录
     *
     * @return 解压的目录
     *
     * @throws UtilsException IO 异常
     */
    public static File unzip(String zipFilePath, String outFileDir) throws UtilsException {
        return unzip(zipFilePath, outFileDir, DEFAULT_CHARSET);
    }

    /**
     * 解压
     *
     * @param zipFilePath 压缩文件的路径
     * @param outFileDir 解压到的目录
     * @param charset 编码
     *
     * @return 解压的目录
     *
     * @throws UtilsException IO 异常
     */
    public static File unzip(String zipFilePath, String outFileDir, Charset charset) throws UtilsException {
        return unzip(FileUtils.file(zipFilePath), FileUtils.mkdir(outFileDir), charset);
    }

    /**
     * 解压，默认使用 UTF-8 编码
     *
     * @param zipFile zip 文件
     * @param outFile 解压到的目录
     *
     * @return 解压的目录
     *
     * @throws UtilsException IO 异常
     */
    public static File unzip(File zipFile, File outFile) throws UtilsException {
        return unzip(zipFile, outFile, DEFAULT_CHARSET);
    }

    /**
     * 解压
     *
     * @param zipFile zip 文件
     * @param outFile 解压到的目录
     * @param charset 编码
     *
     * @return 解压的目录
     *
     * @throws UtilsException IO 异常
     */
    public static File unzip(File zipFile, File outFile, Charset charset) throws UtilsException {
        ZipFile zip;
        try {
            zip = new ZipFile(zipFile, charset);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
        return unzip(zip, outFile);
    }

    /**
     * 解压
     *
     * @param zipFile zip 文件，附带编码信息，使用完毕自动关闭
     * @param outFile 解压到的目录
     *
     * @return 解压的目录
     *
     * @throws IORuntimeException IO 异常
     */
    public static File unzip(ZipFile zipFile, File outFile) throws IORuntimeException {
        try {
            final Enumeration<ZipEntry> em = (Enumeration<ZipEntry>) zipFile.entries();
            ZipEntry zipEntry;
            File outItemFile;
            while (em.hasMoreElements()) {
                zipEntry = em.nextElement();
                // FileUtil.file 会检查 slip 漏洞，漏洞说明见 http://blog.nsfocus.net/zip-slip-2/
                outItemFile = buildFile(outFile, zipEntry.getName());
                if (zipEntry.isDirectory()) {
                    // 创建对应目录
                    outItemFile.mkdirs();
                } else {
                    // 写出文件
                    write(zipFile, zipEntry, outItemFile);
                }
            }
        } finally {
            IoUtils.close(zipFile);
        }
        return outFile;
    }

    /**
     * 解压<br>
     * ZIP 条目不使用高速缓冲
     *
     * @param in zip 文件流，使用完毕自动关闭
     * @param outFile 解压到的目录
     * @param charset 编码
     *
     * @return 解压的目录
     *
     * @throws UtilsException IO 异常
     */
    public static File unzip(InputStream in, File outFile, Charset charset) throws UtilsException {
        if (null == charset) {
            charset = DEFAULT_CHARSET;
        }
        return unzip(new ZipInputStream(in, charset), outFile);
    }

    /**
     * 解压<br>
     * ZIP 条目不使用高速缓冲
     *
     * @param zipStream zip 文件流，包含编码信息
     * @param outFile 解压到的目录
     *
     * @return 解压的目录
     *
     * @throws UtilsException IO 异常
     */
    public static File unzip(ZipInputStream zipStream, File outFile) throws UtilsException {
        try {
            ZipEntry zipEntry;
            File outItemFile;
            while (null != (zipEntry = zipStream.getNextEntry())) {
                // FileUtil.file 会检查 slip 漏洞，漏洞说明见 http://blog.nsfocus.net/zip-slip-2/
                outItemFile = FileUtils.file(outFile, zipEntry.getName());
                if (zipEntry.isDirectory()) {
                    // 目录
                    outItemFile.mkdirs();
                } else {
                    // 文件
                    FileUtils.writeFromStream(zipStream, outItemFile);
                }
            }
        } catch (IOException e) {
            throw new UtilsException(e);
        } finally {
            IoUtils.close(zipStream);
        }
        return outFile;
    }

    /**
     * 从 Zip 文件中提取指定的文件为 bytes
     *
     * @param zipFilePath Zip 文件
     * @param name 文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如 images/aaa.txt
     *
     * @return 文件内容 bytes
     */
    public static byte[] unzipFileBytes(String zipFilePath, String name) {
        return unzipFileBytes(zipFilePath, DEFAULT_CHARSET, name);
    }

    /**
     * 从 Zip 文件中提取指定的文件为 bytes
     *
     * @param zipFilePath Zip 文件
     * @param charset 编码
     * @param name 文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如 images/aaa.txt
     *
     * @return 文件内容 bytes
     */
    public static byte[] unzipFileBytes(String zipFilePath, Charset charset, String name) {
        return unzipFileBytes(FileUtils.file(zipFilePath), charset, name);
    }

    /**
     * 从 Zip 文件中提取指定的文件为 bytes
     *
     * @param zipFile Zip 文件
     * @param name 文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如 images/aaa.txt
     *
     * @return 文件内容 bytes
     */
    public static byte[] unzipFileBytes(File zipFile, String name) {
        return unzipFileBytes(zipFile, DEFAULT_CHARSET, name);
    }

    /**
     * 从 Zip 文件中提取指定的文件为 bytes
     *
     * @param zipFile Zip 文件
     * @param charset 编码
     * @param name 文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如 images/aaa.txt
     *
     * @return 文件内容 bytes
     */
    public static byte[] unzipFileBytes(File zipFile, Charset charset, String name) {
        ZipFile zipFileObj = null;
        try {
            zipFileObj = new ZipFile(zipFile, charset);
            final Enumeration<ZipEntry> em = (Enumeration<ZipEntry>) zipFileObj.entries();
            ZipEntry zipEntry;
            while (em.hasMoreElements()) {
                zipEntry = em.nextElement();
                if ((!zipEntry.isDirectory()) && name.equals(zipEntry.getName())) {
                    return IoUtils.readBytes(zipFileObj.getInputStream(zipEntry));
                }
            }
        } catch (IOException e) {
            throw new UtilsException(e);
        } finally {
            IoUtils.close(zipFileObj);
        }
        return null;
    }

    /**
     * Gzip 压缩处理
     *
     * @param content 被压缩的字符串
     * @param charset 编码
     *
     * @return 压缩后的字节流
     *
     * @throws UtilsException IO 异常
     */
    public static byte[] gzip(String content, String charset) throws UtilsException {
        return gzip(StringUtils.bytes(content, charset));
    }

    /**
     * Gzip 压缩处理
     *
     * @param buf 被压缩的字节流
     *
     * @return 压缩后的字节流
     *
     * @throws UtilsException IO 异常
     */
    public static byte[] gzip(byte[] buf) throws UtilsException {
        return gzip(new ByteArrayInputStream(buf), buf.length);
    }

    /**
     * Gzip 压缩文件
     *
     * @param file 被压缩的文件
     *
     * @return 压缩后的字节流
     *
     * @throws UtilsException IO 异常
     */
    public static byte[] gzip(File file) throws UtilsException {
        BufferedInputStream in = null;
        try {
            in = FileUtils.getInputStream(file);
            return gzip(in, (int) file.length());
        } finally {
            IoUtils.close(in);
        }
    }

    /**
     * Gzip 压缩文件
     *
     * @param in 被压缩的流
     *
     * @return 压缩后的字节流
     *
     * @throws UtilsException IO 异常
     */
    public static byte[] gzip(InputStream in) throws UtilsException {
        return gzip(in, DEFAULT_BYTE_ARRAY_LENGTH);
    }

    /**
     * Gzip 压缩文件
     *
     * @param in 被压缩的流
     * @param length 预估长度
     *
     * @return 压缩后的字节流
     *
     * @throws UtilsException IO 异常
     */
    public static byte[] gzip(InputStream in, int length) throws UtilsException {
        final ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
        GZIPOutputStream gos = null;
        try {
            gos = new GZIPOutputStream(bos);
            IoUtils.copy(in, gos);
        } catch (IOException e) {
            throw new UtilsException(e);
        } finally {
            IoUtils.close(gos);
        }
        // 返回必须在关闭 gos 后进行，因为关闭时会自动执行 finish()方法，保证数据全部写出
        return bos.toByteArray();
    }

    /**
     * Gzip 解压缩处理
     *
     * @param buf 压缩过的字节流
     * @param charset 编码
     *
     * @return 解压后的字符串
     *
     * @throws UtilsException IO 异常
     */
    public static String unGzip(byte[] buf, String charset) throws UtilsException {
        return StringUtils.string(unGzip(buf), charset);
    }

    /**
     * Gzip 解压处理
     *
     * @param buf buf
     *
     * @return bytes
     *
     * @throws UtilsException IO 异常
     */
    public static byte[] unGzip(byte[] buf) throws UtilsException {
        return unGzip(new ByteArrayInputStream(buf), buf.length);
    }

    /**
     * Gzip 解压处理
     *
     * @param in Gzip 数据
     *
     * @return 解压后的数据
     *
     * @throws UtilsException IO 异常
     */
    public static byte[] unGzip(InputStream in) throws UtilsException {
        return unGzip(in, DEFAULT_BYTE_ARRAY_LENGTH);
    }

    /**
     * Gzip 解压处理
     *
     * @param in Gzip 数据
     * @param length 估算长度，如果无法确定请传入 {@link #DEFAULT_BYTE_ARRAY_LENGTH}
     *
     * @return 解压后的数据
     *
     * @throws UtilsException IO 异常
     */
    public static byte[] unGzip(InputStream in, int length) throws UtilsException {
        GZIPInputStream gzi = null;
        FastByteArrayOutputStream bos;
        try {
            gzi = (in instanceof GZIPInputStream) ? (GZIPInputStream) in : new GZIPInputStream(in);
            bos = new FastByteArrayOutputStream(length);
            IoUtils.copy(gzi, bos);
        } catch (IOException e) {
            throw new UtilsException(e);
        } finally {
            IoUtils.close(gzi);
        }
        // 返回必须在关闭 gos 后进行，因为关闭时会自动执行 finish()方法，保证数据全部写出
        return bos.toByteArray();
    }

    /**
     * Zlib 压缩处理
     *
     * @param content 被压缩的字符串
     * @param charset 编码
     * @param level 压缩级别，1~9
     *
     * @return 压缩后的字节流
     */
    public static byte[] zlib(String content, String charset, int level) {
        return zlib(StringUtils.bytes(content, charset), level);
    }

    /**
     * Zlib 压缩文件
     *
     * @param file 被压缩的文件
     * @param level 压缩级别
     *
     * @return 压缩后的字节流
     */
    public static byte[] zlib(File file, int level) {
        BufferedInputStream in = null;
        try {
            in = FileUtils.getInputStream(file);
            return zlib(in, level, (int) file.length());
        } finally {
            IoUtils.close(in);
        }
    }

    /**
     * 打成 Zlib 压缩包
     *
     * @param buf 数据
     * @param level 压缩级别，0~9
     *
     * @return 压缩后的 bytes
     */
    public static byte[] zlib(byte[] buf, int level) {
        return zlib(new ByteArrayInputStream(buf), level, buf.length);
    }

    /**
     * 打成 Zlib 压缩包
     *
     * @param in 数据流
     * @param level 压缩级别，0~9
     *
     * @return 压缩后的 bytes
     */
    public static byte[] zlib(InputStream in, int level) {
        return zlib(in, level, DEFAULT_BYTE_ARRAY_LENGTH);
    }

    /**
     * 打成 Zlib 压缩包
     *
     * @param in 数据流
     * @param level 压缩级别，0~9
     * @param length 预估大小
     *
     * @return 压缩后的 bytes
     */
    public static byte[] zlib(InputStream in, int level, int length) {
        final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
        deflater(in, out, level, false);
        return out.toByteArray();
    }

    /**
     * Zlib 解压缩处理
     *
     * @param buf 压缩过的字节流
     * @param charset 编码
     *
     * @return 解压后的字符串
     */
    public static String unZlib(byte[] buf, String charset) {
        return StringUtils.string(unZlib(buf), charset);
    }

    /**
     * 解压缩 zlib
     *
     * @param buf 数据
     *
     * @return 解压后的 bytes
     */
    public static byte[] unZlib(byte[] buf) {
        return unZlib(new ByteArrayInputStream(buf), buf.length);
    }

    /**
     * 解压缩 zlib
     *
     * @param in 数据流
     *
     * @return 解压后的 bytes
     */
    public static byte[] unZlib(InputStream in) {
        return unZlib(in, DEFAULT_BYTE_ARRAY_LENGTH);
    }

    /**
     * 解压缩 zlib
     *
     * @param in 数据流
     * @param length 预估长度
     *
     * @return 解压后的 bytes
     */
    public static byte[] unZlib(InputStream in, int length) {
        final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
        inflater(in, out, false);
        return out.toByteArray();
    }

    /**
     * 获取 Zip 文件中指定目录下的所有文件，只显示文件，不显示目录
     *
     * @param zipFile Zip 文件
     * @param dir 目录前缀
     *
     * @return 文件列表
     */
    public static List<String> listFileNames(ZipFile zipFile, String dir) {
        if (StringUtils.isNotBlank(dir)) {
            // 目录尾部添加"/"
            dir = StringUtils.addSuffixIfNot(dir, StringUtils.SLASH);
        }

        final List<String> fileNames = new ArrayList<>();
        String name;
        for (ZipEntry entry : Collections.list(zipFile.entries())) {
            name = entry.getName();
            if (StringUtils.isEmpty(dir) || name.startsWith(dir)) {
                final String nameSuffix = StringUtils.removePrefix(name, dir);
                if (StringUtils.isNotEmpty(nameSuffix) && !StringUtils.contains(nameSuffix, CharUtils.SLASH)) {
                    fileNames.add(nameSuffix);
                }
            }
        }

        return fileNames;
    }

    /**
     * 获得 {@link ZipOutputStream}
     *
     * @param zipFile 压缩文件
     * @param charset 编码
     *
     * @return {@link ZipOutputStream}
     */
    private static ZipOutputStream getZipOutputStream(File zipFile, Charset charset) {
        return getZipOutputStream(FileUtils.getOutputStream(zipFile), charset);
    }

    /**
     * 获得 {@link ZipOutputStream}
     *
     * @param out 压缩文件流
     * @param charset 编码
     *
     * @return {@link ZipOutputStream}
     */
    private static ZipOutputStream getZipOutputStream(OutputStream out, Charset charset) {
        return new ZipOutputStream(out, ObjectUtils.defaultIfNull(charset, DEFAULT_CHARSET));
    }

    /**
     * 递归压缩文件夹<br>
     * srcRootDirectory 决定了路径截取的位置，例如：<br>
     * file 的路径为 d:/a/b/c/d.txt，srcRootDirectory 为 d:/a/b，则压缩后的文件与目录为结构为 c/d.txt
     *
     * @param out 压缩文件存储对象
     * @param srcRootDirectory 被压缩的文件夹根目录
     * @param file 当前递归压缩的文件或目录对象
     * @param filter 文件过滤器，通过实现此接口，自定义要过滤的文件（过滤掉哪些文件或文件夹不加入压缩）
     *
     * @throws UtilsException IO 异常
     */
    private static void zip(File file, String srcRootDirectory, ZipOutputStream out, FileFilter filter) throws UtilsException {
        if (null == file || (null != filter && !filter.accept(file))) {
            return;
        }

        // 获取文件相对于压缩文件夹根目录的子路径
        final String subPath = FileUtils.subPath(srcRootDirectory, file);

        // 如果是目录，则压缩压缩目录中的文件或子目录
        if (file.isDirectory()) {
            final File[] files = file.listFiles();
            if (ArrayUtils.isEmpty(files) && StringUtils.isNotEmpty(subPath)) {
                // 加入目录，只有空目录时才加入目录，非空时会在创建文件时自动添加父级目录
                addDirectory(subPath, out);
            }
            // 压缩目录下的子文件或目录
            //noinspection ConstantConditions
            for (File childFile : files) {
                zip(childFile, srcRootDirectory, out, filter);
            }
        } else {// 如果是文件或其它符号，则直接压缩该文件
            addFile(file, subPath, out);
        }
    }

    /**
     * 添加文件到压缩包
     *
     * @param file 需要压缩的文件
     * @param path 在压缩文件中的路径
     * @param out 压缩文件存储对象
     *
     * @throws UtilsException IO 异常
     */
    private static void addFile(File file, String path, ZipOutputStream out) throws UtilsException {
        addFile(FileUtils.getInputStream(file), path, out);
    }

    /**
     * 添加文件流到压缩包，添加后关闭流
     *
     * @param in 需要压缩的输入流
     * @param path 压缩的路径
     * @param out 压缩文件存储对象
     *
     * @throws UtilsException IO 异常
     */
    private static void addFile(InputStream in, String path, ZipOutputStream out) throws UtilsException {
        if (null == in) {
            return;
        }
        try {
            out.putNextEntry(new ZipEntry(path));
            IoUtils.copy(in, out);
        } catch (IOException e) {
            throw new UtilsException(e);
        } finally {
            IoUtils.close(in);
            closeEntry(out);
        }
    }

    /**
     * 在压缩包中新建目录
     *
     * @param path 压缩的路径
     * @param out 压缩文件存储对象
     *
     * @throws UtilsException IO 异常
     */
    private static void addDirectory(String path, ZipOutputStream out) throws UtilsException {
        path = StringUtils.addSuffixIfNot(path, StringUtils.SLASH);
        try {
            out.putNextEntry(new ZipEntry(path));
        } catch (IOException e) {
            throw new UtilsException(e);
        } finally {
            closeEntry(out);
        }
    }

    /**
     * 判断压缩文件保存的路径是否为源文件路径的子文件夹，如果是，则抛出异常（防止无限递归压缩的发生）
     *
     * @param zipFile 压缩后的产生的文件路径
     * @param srcFiles 被压缩的文件或目录
     */
    private static void validateFiles(File zipFile, File... srcFiles) throws UtilsException {
        if (zipFile.isDirectory()) {
            throw new UtilsException("Zip file [{}] must not be a directory !", zipFile.getAbsoluteFile());
        }

        for (File srcFile : srcFiles) {
            if (null == srcFile) {
                continue;
            }
            if (!srcFile.exists()) {
                throw new UtilsException(StringUtils.format("File [{}] not exist!", srcFile.getAbsolutePath()));
            }

            try {
                final File parentFile = zipFile.getCanonicalFile().getParentFile();
                // 压缩文件不能位于被压缩的目录内
                if (srcFile.isDirectory() && parentFile.getCanonicalPath().contains(srcFile.getCanonicalPath())) {
                    throw new UtilsException("Zip file path [{}] must not be the child directory of [{}] !", zipFile.getCanonicalPath(), srcFile.getCanonicalPath());
                }

            } catch (IOException e) {
                throw new UtilsException(e);
            }
        }
    }

    /**
     * 关闭当前 Entry，继续下一个 Entry
     *
     * @param out ZipOutputStream
     */
    private static void closeEntry(ZipOutputStream out) {
        try {
            out.closeEntry();
        } catch (IOException e) {
            // ignore
        }
    }

    /**
     * 从 Zip 中读取文件流并写出到文件
     *
     * @param zipFile Zip 文件
     * @param zipEntry zip 文件中的子文件
     * @param outItemFile 输出到的文件
     *
     * @throws IORuntimeException IO 异常
     */
    private static void write(ZipFile zipFile, ZipEntry zipEntry, File outItemFile) throws IORuntimeException {
        InputStream in = null;
        try {
            in = zipFile.getInputStream(zipEntry);
            FileUtils.writeFromStream(in, outItemFile);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        } finally {
            IoUtils.close(in);
        }
    }

    /**
     * 将 Zlib 流解压到 out 中
     *
     * @param in zlib 数据流
     * @param out 输出
     * @param nowrap true 表示兼容 Gzip 压缩
     */
    private static void inflater(InputStream in, OutputStream out, boolean nowrap) {
        final InflaterOutputStream ios = (out instanceof InflaterOutputStream) ? (InflaterOutputStream) out : new InflaterOutputStream(out, new Inflater(nowrap));
        IoUtils.copy(in, ios);
        try {
            ios.finish();
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * 将普通数据流压缩成 zlib 到 out 中
     *
     * @param in zlib 数据流
     * @param out 输出
     * @param level 压缩级别，0~9
     * @param nowrap true 表示兼容 Gzip 压缩
     */
    private static void deflater(InputStream in, OutputStream out, int level, boolean nowrap) {
        final DeflaterOutputStream ios = (out instanceof DeflaterOutputStream) ? (DeflaterOutputStream) out : new DeflaterOutputStream(out, new Deflater(level, nowrap));
        IoUtils.copy(in, ios);
        try {
            ios.finish();
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * 根据压缩包中的路径构建目录结构，在 Win 下直接构建，在 Linux 下拆分路径单独构建
     *
     * @param outFile 最外部路径
     * @param fileName 文件名，可以包含路径
     *
     * @return 文件或目录
     */
    private static File buildFile(File outFile, String fileName) {
        if (!FileUtils.isWindows()
                // 检查文件名中是否包含"/"，不考虑以"/"结尾的情况
                && fileName.lastIndexOf(CharUtils.SLASH, fileName.length() - 2) > 0) {
            // 在 Linux 下多层目录创建存在问题，/会被当成文件名的一部分，此处做处理
            // 使用/拆分路径（zip 中无\），级联创建父目录
            final String[] pathParts = StringUtils.splitToArray(fileName, CharUtils.SLASH);
            for (int i = 0; i < pathParts.length - 1; i++) {
                // 由于路径拆分，slip 不检查，在最后一步检查
                outFile = new File(outFile, pathParts[i]);
            }
            outFile.mkdirs();
            // 最后一个部分如果非空，作为文件名
            fileName = pathParts[pathParts.length - 1];
        }
        return FileUtils.file(outFile, fileName);
    }

}